#include "../initc.h"
#include "../ui.h"
#include "c_dma.h"
#include "memtable.h"


u1 AddrNoIncr = 0;


static u1 read_reg(eop* const reg, u2 const address)
{
	u1 al;
	asm volatile("call %A1" : "=a" (al) : "rm" (reg), "c" (address) : "cc", "memory", "ebx");
	return al;
}


static void transdmappu2cpu(u1 const al, DMAInfo* const esi)
{
	// set address increment value
	s4 const addrincr =
		al & 0x08 ?  0 :
		al & 0x10 ? -1 : // Automatic decrement
		1;               // Automatic increment

	// get address order to be written
	static u1 const addrwrite[][4] =
	{
		{ 0, 0, 0, 0 },
		{ 0, 1, 0, 1 },
		{ 0, 0, 0, 0 },
		{ 0, 0, 1, 1 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 }
	};
	u1 const* const edi = addrwrite[al & 0x07];

	// Pointer address of registers
	eop* const regptr_ = REGPTR(0x2100 + esi->destination + edi[0]); // PPU memory - 21xx
	eop* const regptrb = REGPTR(0x2100 + esi->destination + edi[1]); // PPU memory - 21xx
	eop* const regptrc = REGPTR(0x2100 + esi->destination + edi[2]); // PPU memory - 21xx
	eop* const regptrd = REGPTR(0x2100 + esi->destination + edi[3]); // PPU memory - 21xx

	u2 const dx      = esi->count;
	u1 const curbank = esi->bank;
	u2       cx      = esi->offset;
	esi->count = 0;

#if 0 // XXX seems to be unused in the loop
	u1 const* const esi = (cx & 0x8000 ? snesmmap : snesmap2)[curbank];
#endif

	// Do loop
	u4 edx = dx != 0 ? dx : 65536;
	while (edx > 4)
	{
		memw8no_rom(curbank, cx, read_reg(regptr_, cx));
		cx += addrincr;
		memw8no_rom(curbank, cx, read_reg(regptrb, cx));
		cx += addrincr;
		memw8no_rom(curbank, cx, read_reg(regptrc, cx));
		cx += addrincr;
		memw8no_rom(curbank, cx, read_reg(regptrd, cx));
		cx += addrincr;
		edx -= 4;
	}
	memw8no_rom(curbank, cx, read_reg(regptr_, cx));
	cx += addrincr;
	if (--edx != 0)
	{
		memw8no_rom(curbank, cx, read_reg(regptrb, cx));
		cx += addrincr;
		if (--edx != 0)
		{
			memw8no_rom(curbank, cx, read_reg(regptrc, cx));
			cx += addrincr;
			if (--edx != 0)
			{
				memw8no_rom(curbank, cx, read_reg(regptrd, cx));
				cx += addrincr;
			}
		}
	}

	esi->offset = cx;
}


static inline void write_reg(eop* const reg, u2 const address, u1 const val)
{
	asm volatile("call %A0" :: "rm" (reg), "c" (address), "a" (val) : "cc", "memory", "ebx");
}


static void transdma(DMAInfo* const esi)
{
	u1 const al = esi->control;
	if (al & 0x80)
	{
		transdmappu2cpu(al, esi);
		return;
	}

	// Set address increment value
	s4 const addrincr =
		al & 0x08 ?  0 :
		al & 0x10 ? -1 : // Automatic decrement
		1;               // Automatic increment
	AddrNoIncr = addrincr == 0;

	// Get address order to be written
	u1 mode = al & 0x07;
	if (mode == 5) mode -= 4; // Mode 5 DMA
	static u1 const addrwrite[][4] =
	{
		{ 0, 0, 0, 0 },
		{ 0, 1, 0, 1 },
		{ 0, 0, 0, 0 },
		{ 0, 0, 1, 1 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 }
	};
	u1 const* const edi = addrwrite[mode];

	// Pointer address of registers
	eop* const regptra = REGPTW(0x2100 + esi->destination + edi[0]); // PPU memory - 21xx
	eop* const regptrb = REGPTW(0x2100 + esi->destination + edi[1]); // PPU memory - 21xx
	eop* const regptrc = REGPTW(0x2100 + esi->destination + edi[2]); // PPU memory - 21xx
	eop* const regptrd = REGPTW(0x2100 + esi->destination + edi[3]); // PPU memory - 21xx

	u2 const dx      = esi->count;
	u1 const curbank = esi->bank;
	u2       cx      = esi->offset;
	esi->count = 0;

#if 0 // XXX seems to be unused in the loop
	u1 const* const esi = (cx & 0x8000 ? snesmmap : snesmap2)[curbank];
#endif

	// Do loop
	u4 edx = dx != 0 ? dx : 65536;
	while (edx > 4)
	{
		u1 const vala = memr8(curbank, cx);
		write_reg(regptra, cx += addrincr, vala);
		u1 const valb = memr8(curbank, cx);
		write_reg(regptrb, cx += addrincr, valb);
		u1 const valc = memr8(curbank, cx);
		write_reg(regptrc, cx += addrincr, valc);
		u1 const vald = memr8(curbank, cx);
		write_reg(regptrd, cx += addrincr, vald);
		edx -= 4;
	}
	u1 const vala = memr8(curbank, cx);
	write_reg(regptra, cx += addrincr, vala);
	if (--edx != 0)
	{
		u1 const valb = memr8(curbank, cx);
		write_reg(regptrb, cx += addrincr, valb);
		if (--edx != 0)
		{
			u1 const valc = memr8(curbank, cx);
			write_reg(regptrc, cx += addrincr, valc);
			if (--edx != 0)
			{
				u1 const vald = memr8(curbank, cx);
				write_reg(regptrd, cx += addrincr, vald);
			}
		}
	}

	esi->offset = cx;
	AddrNoIncr  = 0;
}


void c_reg420Bw(u4 eax)
{
	DMAInfo* esi = dmadata;
	for (eax &= 0xFF; eax != 0; ++esi, eax >>= 1)
	{
		if (eax & 0x01) transdma(esi);
	}
}


void setuphdma(u4 const ah, HDMAInfo* const edx, DMAInfo* const esi)
{
	// transfer old address to new address
	u2 const ax = esi->offset;
	esi->hdma_table = ax;
	edx->addr_inc   = ax;

	// Get address order to be written
	u1 mode = esi->control & 0x07;
	if (mode >= 5) mode -= 4; // Mode 5, 6 or 7
	static u1 const addrnumt[] = { 1, 2, 2, 4, 4, 4, 4, 4 };
	edx->count = addrnumt[mode];
	static u1 const addrwrite[][4] =
	{
		{ 0, 0, 0, 0 },
		{ 0, 1, 0, 1 },
		{ 0, 0, 0, 0 },
		{ 0, 0, 1, 1 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 }
	};
	u1 const* const edi = addrwrite[mode];

	// Get pointers
	u2 const base_addr = 0x2100 + esi->destination;
	for (u4 i = 0; i != lengthof(edx->dst_reg); ++i)
	{
		u2 bx = base_addr + edi[i]; // PPU memory - 21xx
		if (bx == 0x2118 || bx == 0x2119) bx = 0x2200; // Bad hack _Demo_
		edx->dst_reg[i] = REGPTW(bx);
	}

	esi->hdma_line_counter = 0;
	hdmatype              |= ah;
}


void c_reg420Cw(u4 eax)
{
	u1 const al = eax;
	curhdma = al;
	if (curypos < resolutn &&
			(!(INTEnab & 0x10) || (80 <= HIRQLoc && HIRQLoc <= 176)) &&
			nexthdma & al == 0)
	{
		nexthdma = al;
		DMAInfo*  esi = dmadata;
		HDMAInfo* edx = hdmadata;
		for (u1 i = 0x01; i != 0; ++esi, ++edx, i <<= 1)
		{
			if (al & i) setuphdma(i, edx, esi);
		}
	}
	if (nohdmaframe == 1) ++hdmadelay;
	hdmarestart = 0;
}


static void setuphdmars(HDMAInfo* const edx, DMAInfo const* const esi)
{
	// get address order to be written
	u1 mode = esi->control & 0x07;
	if (mode >= 5) mode -= 4; // Mode 5, 6 or 7;
	static u1 const addrnumt[] = { 1, 2, 2, 4, 4, 4, 4, 4 };
	edx->count = addrnumt[mode];
	static u1 const addrwrite[][4] =
	{
		{ 0, 0, 0, 0 },
		{ 0, 1, 0, 1 },
		{ 0, 0, 0, 0 },
		{ 0, 0, 1, 1 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 },
		{ 0, 1, 2, 3 }
	};
	u1 const* const edi = addrwrite[mode];

	// Get pointers
	u2 const base_addr = 0x2100 + esi->destination;
	for (u4 i = 0; i != lengthof(edx->dst_reg); ++i)
	{
		u2 bx = base_addr + edi[i]; // PPU memory - 21xx
		if (bx == 0x2118 || bx == 0x2119) bx = 0x2200; // Bad hack _Demo_
		edx->dst_reg[i] = REGPTW(bx);
	}
}


void starthdma(void)
{
	u1 const al = curhdma;
	nexthdma = al;
	if (al == 0x00) return;

	DMAInfo*  esi = dmadata;
	HDMAInfo* edx = hdmadata;
	for (u1 i = 0x01; i != 0; ++esi, ++edx, i <<= 1)
	{
		if (al & i) setuphdma(i, edx, esi);
	}
}


static void hdmatype2indirect(HDMAInfo const* const edx, DMAInfo* const esi)
{
	u1          tempdecr = edx->count;
	eop* const* reg      = edx->dst_reg;
	do
	{
		u2 const cx = esi->count++; // increment/decrement/keep pointer location
		u1 const al = memr8(esi->hdma_bank, cx);
		write_reg(*reg, cx, al);
	}
	while (++reg, --tempdecr != 0);

	--esi->hdma_line_counter;
}


static void indirectaddr(u4 const ah, HDMAInfo* const edx, DMAInfo* const esi)
{
	if ((esi->hdma_line_counter & 0x7F) == 0)
	{
		if (!(hdmatype & ah)) edx->addr_inc += 2;
    hdmatype &= ~ah;

		u1 const al = memr8(esi->bank, edx->addr_inc++);
		esi->hdma_line_counter = al;
		esi->count             = memr16(esi->bank, edx->addr_inc);
		if (al == 0)
		{
			nexthdma ^= ah;
			esi->hdma_table = edx->addr_inc;
			return;
		}

		if (esi->hdma_line_counter > 0x80) goto hdmatype2indirect;

		u1          tempdecr = edx->count;
		u2          cx       = esi->count; // increment/decrement/keep pointer location
		eop* const* reg      = edx->dst_reg;
		do
		{
			u1 const al = memr8(esi->hdma_bank, cx);
			write_reg(*reg, cx, al);
		}
		while (++cx, ++reg, --tempdecr != 0);
	}
	else if (esi->hdma_line_counter & 0x80)
	{
hdmatype2indirect:
		hdmatype2indirect(edx, esi);
		return;
	}

	esi->hdma_table = edx->addr_inc;
	--esi->hdma_line_counter;
}


static void hdmatype2(HDMAInfo* const edx, DMAInfo* const esi)
{
	u1          tempdecr = edx->count;
	eop* const* reg      = edx->dst_reg;
	do
	{
		u2 const cx = edx->addr_inc++; // increment/decrement/keep pointer location
		u1 const al = memr8(esi->bank, cx);
		write_reg(*reg, cx, al);
	}
	while (++reg, --tempdecr != 0);

	esi->hdma_table = edx->addr_inc;
	--esi->hdma_line_counter;
}


static void dohdma(u4 const ah, HDMAInfo* const edx, DMAInfo* const esi)
{
	if (esi->control & 0x40)
	{
		indirectaddr(ah, edx, esi);
		return;
	}

	if ((esi->hdma_line_counter & 0x7F) == 0)
	{
		if (!(esi->hdma_line_counter & 0x80) && !(hdmatype & ah)) edx->addr_inc += edx->count; // Increment
		hdmatype &= ~ah;

		u1 const al = memr8(esi->bank, edx->addr_inc++);
		esi->hdma_line_counter = al;

		if (al == 0)
		{
			nexthdma ^= ah;
			esi->hdma_table = edx->addr_inc;
			return;
		}

		if (esi->hdma_line_counter > 0x80) goto hdmatype2;

		u1          tempdecr = edx->count;
		u2          cx       = edx->addr_inc;
		eop* const* reg      = edx->dst_reg;
		do
		{
			u1 const al = memr8(esi->bank, cx);
			write_reg(*reg, cx, al);
		}
		while (++cx, ++reg, --tempdecr != 0);
	}
	else if (esi->hdma_line_counter & 0x80)
	{
hdmatype2:
		hdmatype2(edx, esi);
		return;
	}

	esi->hdma_table = edx->addr_inc;
	--esi->hdma_line_counter;
}


static void exechdmars(void)
{
	u1 const al = nexthdma;
	if (al != 0x00)
	{
		DMAInfo*  esi = dmadata;
		HDMAInfo* edx = hdmadata;
		for (u1 i = 0x01; i != 0; ++esi, ++edx, i <<= 1)
		{
			if (!(al & i)) continue;
			setuphdmars(edx, esi);
			dohdma(i, edx, esi);
		}
	}
	hdmarestart = 0;
}


void exechdma(void)
{
	if (hdmarestart == 1)
	{
		exechdmars();
		return;
	}

	u1 const al = nexthdma;
	if (al == 0x00) return;

	DMAInfo*  esi = dmadata;
	HDMAInfo* edx = hdmadata;
	for (u1 i = 0x01; i != 0; ++esi, ++edx, i <<= 1)
	{
		if (al & i) dohdma(i, edx, esi);
	}
}
